/**
 * Project: Useful Programs
 * Package: tools
 */
package org.leumasjaffe.dice;

import java.util.Random;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.leumasjaffe.collections.Swap;
import org.leumasjaffe.math.ArrayMath;
import org.leumasjaffe.math.probability.UniformRV;



/**
 * 
 * Class Dice in file Dice.java <br>
 * Represents a certain number of n-sided dice that can be rolled, <br>
 * Allows the simulation of dice rolling for use in situations like those in d20 games.
 * 
 * @author leumasjaffe
 * @version Jan 22, 2013
 * Created on: Jul 26, 2012
 *
 */
/**
 * Class .Dice in file Dice.java <br>
 * 
 * @author leumasjaffe
 * @version Jan 22, 2012
 * Created on: Jan 22, 2013
 * 
 */
@NonNullByDefault
public class Dice extends AbsRoller{
	/** A four sided die */
	public static final Dice _d4 = new Dice(4);
	
	/** A six sided die */
	public static final Dice _d6 = new Dice(6);
	
	/** A eight sided die */
	public static final Dice _d8 = new Dice(8);
	
	/** Two four sided die */
	public static final Dice _2d4 = new Dice(2, 4);
	
	/** A ten sided die */
	public static final Dice _d10 = new Dice(10);
	
	/** A twelve sided die */
	public static final Dice _d12 = new Dice(12);
	
	/** Two six sided die */
	public static final Dice _2d6 = new Dice(2, 6);
	
	/** Two eight sided die */
	public static final Dice _2d8 = new Dice(2, 8);
	
	/** Three six sided die */
	public static final Dice _3d6 = new Dice(3, 6);
	
	/** A twenty sided die */
	public static final Dice _d20 = new Dice(20);
	
	/** A 100-sided die */
	public static final Dice _dPercent = new Dice(100);
	
	/** An alias to the _dPercent object */
	public static final Dice _d100 = _dPercent;
	
	/**
	 * A regex that determines if a string is analogous to a die roll
	 */
	private static final String isDieRoll = "\\d*d\\d*( ?[\\+\\-] ?\\d*)?";
	
	/**
	 * The uniform random variable that governs die rolls
	 */
	private final UniformRV roller;
	
	/**
	 * The number of dice that are being rolled
	 */
	private final int numDice;
	
	/**
	 * The default bonus to the die roll
	 */
	private final int rollBonus;
	
	/**
	 * The number of sides that this die has
	 */
	private final int numSides;
	
	/**
	 * Constructs a Dice instance with the given number of sides, dice to roll, and bonus to rolls
	 * @param rand The random number generator that provides rolls for this instance<br>
	 * The default Random if it is not manually provided is seeded with the current system time
	 * @param numDice The number of dice that this instance will roll
	 * @param numSides The number of sides that each Die rolled has
	 * @param rollBonus The default bonus added to the total roll from this instance
	 */
	public Dice(Random rand, int numDice, int numSides, int rollBonus) {
		this.numDice = Math.max(numDice, 1);
		this.numSides = Math.max(numSides, 1);
		this.roller = new UniformRV(rand, 1, this.numSides);
		this.rollBonus = rollBonus;
	}
	
	/**
	 * Constructs a Dice instance with the given number of sides, dice to roll, and bonus to rolls
	 * @param numDice The number of dice that this instance will roll
	 * @param numSides The number of sides that each Die rolled has
	 * @param rollBonus The default bonus added to the total roll from this instance
	 */
	public Dice(int numDice, int numSides, int rollBonus) {
		this(new Random(System.currentTimeMillis()), numDice, numSides, rollBonus);
	}
	
	/**
	 * Constructs a Dice instance with the given number of sides and dice to roll
	 * @param rand The random number generator that provides rolls for this instance<br>
	 * The default Random if it is not manually provided is seeded with the current system time
	 * @param numDice The number of dice that this instance will roll
	 * @param numSides The number of sides that each Die rolled has
	 */
	public Dice(Random rand, int numDice, int numSides) {
		this(rand, numDice, numSides, 0);
	}
	
	/**
	 * Constructs a Dice instance with the given number of sides and dice to roll
	 * @param numDice The number of dice that this instance will roll
	 * @param numSides The number of sides that each Die rolled has
	 */
	public Dice(int numDice, int numSides) {
		this(numDice, numSides, 0);
	}
	
	/**
	 * Constructs a Dice instance with the given number of sides
	 * @param rand The random number generator that provides rolls for this instance<br>
	 * The default Random if it is not manually provided is seeded with the current system time
	 * @param numSides The number of sides that each Die rolled has
	 */
	public Dice(Random rand, int numSides) {
		this(rand, 1, numSides);
	}
	
	/**
	 * Constructs a Dice instance with the given number of sides
	 * @param numSides The number of sides that each Die rolled has
	 */
	public Dice(int numSides) {
		this(1, numSides);
	}
	
	/**
	 * Constructs a Dice instance with the default(6) number of sides
	 * @param rand The random number generator that provides rolls for this instance<br>
	 * The default Random if it is not manually provided is seeded with the current system time
	 */
	public Dice(Random rand) {
		this(rand, 6);
	}
	
	/**
	 * Constructs a Dice instance with the default(6) number of sides
	 */
	public Dice() {
		this(6);
	}
	
	/**
	 * Parses a String of the format 'ndx +/- b' into a Dice Object
	 * @param parse The string to be parsed into a die
	 * @return Returns a Dice object constructed from the data provided
	 */
	public static Dice parseRoll(String parse) {
		if (parse.matches(isDieRoll)) {
			int dievider = parse.indexOf('d');
			final int numDice = Integer.parseInt(parse.substring(0, dievider++));
			int side = 0, bonus = 0;
			if (parse.contains("+") || parse.contains("-")) {
				int bonusPart = parse.indexOf(' ');
				boolean space = true;
				if (bonusPart == -1) {
					space = false;
					if ((bonusPart = parse.indexOf('+')) == -1) {
						bonusPart = parse.indexOf('-');
					}
				}
				side = Integer.parseInt(parse.substring(dievider, bonusPart++));
				bonus = Integer.parseInt(parse.substring(bonusPart + (space ? 2 : 0)));
			}
			else {
				side = Integer.parseInt(parse.substring(dievider));
			}
			return new Dice(numDice, side, bonus);
		}
		throw new IllegalArgumentException("This is not a Di(c)e");
	}
	
	/**
	 * Determines whether or not a given String corresponds to a die
	 * @param parse The String to be checked
	 * @return Returns true if the argument matches a die-determining regex
	 */
	public static boolean isDice(String parse) {
		return parse.matches(isDieRoll);
	}
	
	/**
	 * Generates an array of each die roll, the array will have a size of this Dice's numDice field
	 * @return Returns an array of roll results, for roll decomposition
	 */
	private int[] roll_ind() {
		final int[] result = new int[this.numDice];
		for (int i = 0; i < this.numDice; i++) {
			result[i] = (int) this.roller.getX();
		}
		return result;
	}
	
	/**
	 * Rolls the dice, dropping some of the results
	 * @param numDrop The number of low dice to drop
	 * @param bonus A bonus to the roll
	 * @param print A boolean representing whether to print this original roll, 
	 * dropped dice, and results
	 * @return Returns The result of the roll
	 */
	private int roll_drop(int numDrop, int bonus, boolean print) {
		bonus += this.rollBonus;
		if (numDrop >= this.numDice) {
			return bonus;
		}
		else {
			final int[] use = new int[this.numDice - numDrop];
			final int[] rolls = roll_ind();
			if (print) {
				System.out.println("Roll: " + ArrayMath.script_rep(rolls));
			}
			for (int i = 0; i < numDrop; i++) {
				Swap.swap(use, i, rolls, ArrayMath.maxIndex(rolls));
			}
			if (print) {
				System.out.println("Dropped: " + ArrayMath.script_rep(rolls).replaceAll("0", "")
						.replaceAll(", ,", ""));
				System.out.println("Result of Top " + (this.numDice - numDrop) + ": " + 
						rollAsString(use, bonus));
			}
			return ArrayMath.sum(use) + bonus;
		}
	}
	
	/**
	 * Rolls the dice, dropping some of the results
	 * @param numDrop The number of low dice to drop
	 * @param bonus A bonus to the roll
	 * @return The result of the roll
	 */
	public int roll_drop(int numDrop, int bonus) {
		return roll_drop(numDrop, bonus, false);
	}
	
	/**
	 * Rolls the dice, dropping some of the results
	 * @param numDrop The number of low dice to drop
	 * @return The result of the roll
	 */
	public int roll_drop(int numDrop) {
		return roll_drop(numDrop, 0, false);
	}
	
	/**
	 * Rolls the dice, dropping some of the results and printing the process
	 * @param numDrop The number of low dice to drop
	 * @param bonus A bonus to the roll
	 * @return The result of the roll
	 */
	public int roll_drop_print(int numDrop, int bonus) {
		return roll_drop(numDrop, bonus, true);
	}
	
	/**
	 * Rolls the dice, dropping some of the results and printing the process
	 * @param numDrop The number of low dice to drop
	 * @return The result of the roll
	 */
	public int roll_drop_print(int numDrop) {
		return roll_drop(numDrop, 0, true);
	}
	
	/**
	 * Generates a String representing a roll of this Dice instance
	 * @param bonus A bonus to the roll
	 * @return Returns a string representing the individual parts and sum of a new roll
	 */
	public String rollToString(int bonus) {
		final int[] rolls = roll_ind();
		bonus += this.rollBonus;
		return rollAsString(rolls, bonus);
	}
	
	/**
	 * Generates a String representing a roll of this Dice instance
	 * @return Returns a string representing the individual parts and sum of a new roll
	 */
	public String rollToString() {
		return rollToString(0);
	}
	
	/**
	 * Generates a String representing a roll dice
	 * @param rolls An array containing the individual Die rolls 
	 * @param bonus A bonus to the roll
	 * @return Returns a string representing the individual parts and sum of a given roll
	 */
	private static String rollAsString(int[] rolls, int bonus) {
		return ArrayMath.script_rep(rolls) + " + " + bonus + " = " + (ArrayMath.sum(rolls) + bonus);
	}
	
	/* (non-Javadoc)
	 * @see org.leumasjaffe.dice.Roll#roll()
	 */
	@Override
	public int roll() {
		return ArrayMath.sum(roll_ind()) + this.rollBonus;
	}
	
	/* (non-Javadoc)
	 * @see org.leumasjaffe.dice.Roll#roll(int)
	 */
	@Override
	public int roll(int bonus) {
		return roll() + bonus;
	}
	
	/**
	 * Gets the number of sides
	 * @return Returns the number of sides of this di(c)e
	 */
	public int getNumSides() {
		return this.numSides;
	}
	
	/**
	 * Gets the number of dice
	 * @return Returns the number of dice that this Dice instance will be rolling
	 */
	public int getNumDice() {
		return this.numDice;
	}
	
	/**
	 * Gets the default bonus to all rolls by this dice object
	 * @return Returns the default bonus to each roll
	 */
	public int getRollBonus() {
		return this.rollBonus;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return this.numDice + "d" + this.numSides + (this.rollBonus != 0 ? this.rollBonus < 0 ? 
				" - " + -this.rollBonus : " + " + this.rollBonus : ""); 
	}

}
